if(!dojo._hasResource["dojox.data.ItemExplorer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.data.ItemExplorer"] = true;
dojo.provide("dojox.data.ItemExplorer");
dojo.require("dijit.Tree");
dojo.require("dijit.Dialog");
dojo.require("dijit.Menu");
dojo.require("dijit.form.ValidationTextBox");
dojo.require("dijit.form.Textarea");
dojo.require("dijit.form.Button");
dojo.require("dijit.form.CheckBox");
dojo.require("dijit.form.FilteringSelect");

(function(){
    var getValue = function(store, item, prop){
        var value = store.getValues(item, prop);
        if(value.length < 2){
            value = store.getValue(item, prop);
        }
        return value;
    }
    
dojo.declare("dojox.data.ItemExplorer", dijit.Tree, {
    useSelect: false,
    refSelectSearchAttr: null,
	constructor: function(options){
        dojo.mixin(this, options);
		var self = this;
		var initialRootValue = {};
		var root = this.rootModelNode = {value:initialRootValue,id:"root"};
		
		this._modelNodeIdMap = {};
		this._modelNodePropMap = {};
		var nextId = 1;
		this.model = {
			getRoot: function(onItem){
				onItem(root);
			},
			mayHaveChildren: function(modelNode){
				return modelNode.value && typeof modelNode.value == 'object' && !(modelNode.value instanceof Date);
			},
			getChildren: function(parentModelNode, onComplete, onError){
				var keys, parent, item = parentModelNode.value;
				var children = [];
				if(item == initialRootValue){
					onComplete([]);
					return;
				}
				var isItem = self.store && self.store.isItem(item, true);
				if(isItem && !self.store.isItemLoaded(item)){
					// if it is not loaded, do so now.
					self.store.loadItem({
						item:item,
						onItem:function(loadedItem){
							item = loadedItem;
							enumerate();
						}
					});
				}else{
					enumerate();
				}
				function enumerate(){
					// once loaded, enumerate the keys
					if(isItem){
						// get the properties through the dojo data API
						keys = self.store.getAttributes(item);
	                    parent = item;
					}else if(item && typeof item == 'object'){
	                    parent = parentModelNode.value;
						keys = [];
						// also we want to be able to drill down into plain JS objects/arrays
						for(var i in item){
							if(item.hasOwnProperty(i) && i != '__id' && i != '__clientId'){
								keys.push(i);
							}
						}
					}
					if(keys){
						for(var key, k=0; key = keys[k++];){
							children.push({
								property:key, 
								value: isItem ? getValue(self.store, item, key) : item[key],
								parent:  parent});
						}
						children.push({addNew:true, parent: parent, parentNode : parentModelNode});
					}
					onComplete(children);
				}
			},
			getIdentity: function(modelNode){
				if(!modelNode.id){
					if(modelNode.addNew){
						modelNode.property = "--addNew";
					}
					modelNode.id = nextId++;
					if(self.store){
						if(self.store.isItem(modelNode.value)){
							var identity = self.store.getIdentity(modelNode.value);
							(self._modelNodeIdMap[identity] = self._modelNodeIdMap[identity] || []).push(modelNode); 
						}
						if(modelNode.parent){
							identity = self.store.getIdentity(modelNode.parent) + '.' + modelNode.property;
							(self._modelNodePropMap[identity] = self._modelNodePropMap[identity] || []).push(modelNode);
						}
					}
				}
				return modelNode.id;
			},
			getLabel: function(modelNode){
				return modelNode === root ?
						"Object Properties" : 
							modelNode.addNew ? (modelNode.parent instanceof Array ? "Add new value" : "Add new property") : 
								modelNode.property + ": " + 
                                    (modelNode.value instanceof Array ? "(" + modelNode.value.length + " elements)" : modelNode.value);
			},
			onChildrenChange: function(modelNode){
			},
			onChange: function(modelNode){
			}
		};
	},
	postCreate: function(){
		this.inherited(arguments);
		// handle the clicking on the "add new property item"
		dojo.connect(this, "onClick", function(modelNode, treeNode){
			this.lastFocused = treeNode;
			if(modelNode.addNew){
                //this.focusNode(treeNode.getParent());
                this._addProperty();
			}else{
                this._editProperty();
            }
		});
        var contextMenu = new dijit.Menu({
            targetNodeIds: [this.rootNode.domNode], 
            id: "contextMenu"
            });
        dojo.connect(contextMenu, "_openMyself", this, function(e){
            var node = dijit.getEnclosingWidget(e.target);
            if(node){
                var item = node.item;
                if(this.store.isItem(item.value, true) && !item.parent){
                    contextMenu.getChildren().forEach(function(widget){
                        widget.attr("disabled", (widget.label != "Add"));
                    });
                    this.lastFocused = node;
                    // TODO: Root Node - allow Edit when mutli-value editing is possible
                }else if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){
                    // an object that's not a Date - could be a store item
                    contextMenu.getChildren().forEach(function(widget){
                        widget.attr("disabled", (widget.label != "Add") && (widget.label != "Delete"));
                    });
                    this.lastFocused = node;
                    // TODO: Object - allow Edit when mutli-value editing is possible
                }else if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){ // id node
                    this.focusNode(node);
                    alert("Cannot modify an Identifier node.");
                }else if(item.addNew){
                    this.focusNode(node);
                }else{
                    contextMenu.getChildren().forEach(function(widget){
                        widget.attr("disabled", (widget.label != "Edit") && (widget.label != "Delete"));
                    })
                    // this won't focus the node but gives us a way to reference the node
                    this.lastFocused = node;
                }
            }
        });
        contextMenu.addChild(new dijit.MenuItem({label: "Add", onClick: dojo.hitch(this, "_addProperty")}));
        contextMenu.addChild(new dijit.MenuItem({label: "Edit", onClick: dojo.hitch(this, "_editProperty")}));
        contextMenu.addChild(new dijit.MenuItem({label: "Delete", onClick: dojo.hitch(this, "_destroyProperty")}));
        contextMenu.startup();
	},
	store: null,
	setStore: function(store){
		this.store = store;
		var self = this;
        if(this._editDialog){
            this._editDialog.destroyRecursive();
            delete this._editDialog;
        }
        // i think we should just destroy this._editDialog and let _createEditDialog take care of everything 
        // once it gets called again by either _editProperty or _addProperty - it will create everything again
        // using the new store.  this way we don't need to keep track of what is in the dialog if we change it.
		/*if(this._editDialog && this.useSelect){
			dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
	            dijit.getEnclosingWidget(node).attr("store", store);
	        });
		}*/
		dojo.connect(store, "onSet", function(item, attribute, oldValue, newValue){
			var nodes, i, identity = self.store.getIdentity(item);
			nodes = self._modelNodeIdMap[identity];
			
			if(nodes && 
					(oldValue === undefined || newValue === undefined || 
					oldValue instanceof Array || newValue instanceof Array || typeof oldValue == 'object' || typeof newValue == 'object')){
				for(i = 0; i < nodes.length; i++){
					(function(node){
						self.model.getChildren(node, function(children){
							self.model.onChildrenChange(node, children);
						});
					})(nodes[i]);
				}
			}
			nodes = self._modelNodePropMap[identity + "." + attribute];
			
			if(nodes){
				for(i = 0; i < nodes.length; i++){
					nodes[i].value = newValue;
					self.model.onChange(nodes[i]);
				}
			}
		});
		this.rootNode.setChildItems([]);
	},
	setItem: function(item){
		// this is called to show a different item
		
		// reset the maps, for the root getIdentity is not called, so we pre-initialize it here
		(this._modelNodeIdMap = {})[this.store.getIdentity(item)] = [this.rootModelNode];
		this._modelNodePropMap = {};
		
		this.rootModelNode.value = item;
		var self = this;
		this.model.getChildren(this.rootModelNode, function(children){
			self.rootNode.setChildItems(children);
		});
		
	},
	refreshItem: function(){
		this.setItem(this.rootModelNode.value);
	},
    _createEditDialog: function(){
    	this._editDialog = new dijit.Dialog({
           title: "Edit Property",
           execute: dojo.hitch(this, "_updateItem"),
           preload: true
        });
        this._editDialog.placeAt(dojo.body());  
        this._editDialog.startup();
        
        // handle for dialog content
        var pane = dojo.doc.createElement('div');
        
        // label for property
        var labelProp = dojo.doc.createElement('label');
        dojo.attr(labelProp, "for", "property");
        dojo.style(labelProp, "fontWeight", "bold");
        dojo.attr(labelProp, "innerHTML", "Property:")
        pane.appendChild(labelProp);

        // property name field
        var propName = new dijit.form.ValidationTextBox({
            name: "property",
            value: "",
            required: true,
            disabled: true
        }).placeAt(pane);
        
        pane.appendChild(dojo.doc.createElement("br"));
        pane.appendChild(dojo.doc.createElement("br"));
        
        // radio button for "value"
        var value = new dijit.form.RadioButton({
            name: "itemType",
            value: "value",
            onClick: dojo.hitch(this, function(){this._enableFields("value");})
        }).placeAt(pane);
        
        // label for value
        var labelVal = dojo.doc.createElement('label');
        dojo.attr(labelVal, "for", "value");
        dojo.attr(labelVal, "innerHTML", "Value (JSON):")
        pane.appendChild(labelVal);
       
         // container for value fields
        var valueDiv = dojo.doc.createElement("div");
        dojo.addClass(valueDiv, "value");
             
        // textarea
        var textarea = new dijit.form.Textarea({
            name: "jsonVal"
        }).placeAt(valueDiv);
        pane.appendChild(valueDiv);
        
        // radio button for "reference"
        var reference = new dijit.form.RadioButton({
            name: "itemType",
            value: "reference",
            onClick: dojo.hitch(this, function(){this._enableFields("reference");})
        }).placeAt(pane);
        
        // label for reference
        var labelRef = dojo.doc.createElement('label');
        dojo.attr(labelRef, "for", "_reference");
        dojo.attr(labelRef, "innerHTML", "Reference (ID):")
        pane.appendChild(labelRef);
        pane.appendChild(dojo.doc.createElement("br"));
        
        // container for reference fields
        var refDiv = dojo.doc.createElement("div");
        dojo.addClass(refDiv, "reference");
        
        if(this.useSelect){
            // filteringselect
            // TODO: see if there is a way to sort the items in this list
            var refSelect = new dijit.form.FilteringSelect({
                name: "_reference",
                store: this.store,
                searchAttr: this.refSelectSearchAttr || this.store.getIdentityAttributes()[0],
                required: false,
                value: null,        // need to file a ticket about the fetch that happens when declared with value: null
                pageSize: 10
            }).placeAt(refDiv);
        }else{
            var refTextbox = new dijit.form.ValidationTextBox({
                name: "_reference",
                value: "",
                promptMessage: "Enter the ID of the item to reference",
                isValid: dojo.hitch(this, function(isFocused){
                    // don't validate while it's focused
                    return true;//isFocused || this.store.getItemByIdentity(this._editDialog.attr("value")._reference);
                })
            }).placeAt(refDiv);
        }
        pane.appendChild(refDiv);
        pane.appendChild(dojo.doc.createElement("br"));
        pane.appendChild(dojo.doc.createElement("br"));
        
        // buttons
        var buttons = document.createElement('div');
        buttons.setAttribute("dir", "rtl");
        var cancelButton = new dijit.form.Button({type: "reset", label: "Cancel"}).placeAt(buttons);
        cancelButton.onClick = dojo.hitch(this._editDialog, "onCancel");
        var okButton = new dijit.form.Button({type: "submit", label: "OK"}).placeAt(buttons);
        pane.appendChild(buttons);
        
        this._editDialog.attr("content", pane);
    }, 
    _enableFields: function(selection){
        // enables/disables fields based on whether the value in this._editDialog is a reference or a primitive value
        switch(selection){
            case "reference":
                dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){
                    dijit.getEnclosingWidget(node).attr("disabled", true);
                });
                dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
                    dijit.getEnclosingWidget(node).attr("disabled", false);
                });
                break;
            case "value":
                dojo.query(".value [widgetId]", this._editDialog.containerNode).forEach(function(node){
                    dijit.getEnclosingWidget(node).attr("disabled", false);
                });
                dojo.query(".reference [widgetId]", this._editDialog.containerNode).forEach(function(node){
                    dijit.getEnclosingWidget(node).attr("disabled", true);
                });
                break;
        }
    },
    _updateItem: function(vals){
        // a single "execute" function that handles adding and editing of values and references.
        var node, item, val, storeItemVal, editingItem = this._editDialog.attr("title") == "Edit Property";
		var editDialog = this._editDialog;
		var store = this.store;
        function setValue(){
        	try{
	            var itemVal, propPath = [];
	            var prop = vals.property;
	            if(editingItem){
	                while(!store.isItem(item.parent, true)){
	                    node = node.getParent();
	                    propPath.push(item.property);
	                    item = node.item;
	                }
	                if(propPath.length == 0){
	                    // working with an item attribute already
	                    store.setValue(item.parent, item.property, val);
	                }else{
	                    // need to walk back down the item property to the object
	                    storeItemVal = getValue(store, item.parent, item.property);
	                    if(storeItemVal instanceof Array){
	                    	// create a copy for modification
	                    	storeItemVal = storeItemVal.concat();
	                    }
	                    itemVal = storeItemVal;
	                    while(propPath.length > 1){
	                        itemVal = itemVal[propPath.pop()];
	                    }
	                    itemVal[propPath] = val; // this change is reflected in storeItemVal as well
	                    store.setValue(item.parent, item.property, storeItemVal);
	                }              
	            }else{
	                // adding a property
	                if(store.isItem(value, true)){
	                    // adding a top-level property to an item
	    				if (!store.isItemLoaded(value)) {
	                        // fetch the value and see if it is an array
	                        store.loadItem({
	                            item: value,
	                            onItem: function(loadedItem){
	                                if (loadedItem instanceof Array) {
	                                    prop = loadedItem.length;
	                                }
	                                store.setValue(loadedItem, prop, val);
	                            }
	                        });
	                    } else {
	                        if (value instanceof Array) {
	                            prop = value.length;
	                        }
	                        store.setValue(value, prop, val);
	                    }
	                }else{
	                    // adding a property to a lower level in an item
	                    if(item.value instanceof Array){
	                        propPath.push(item.value.length);
	                    }else{
	                        propPath.push(vals.property);
	                    }
	                    while(!store.isItem(item.parent, true)){
	                        node = node.getParent();
	                        propPath.push(item.property);
	                        item = node.item;
	                    }
	                    storeItemVal = getValue(store, item.parent, item.property);
	                    itemVal = storeItemVal;
	                    while(propPath.length > 1){
	                        itemVal = itemVal[propPath.pop()];
	                    }
	                    itemVal[propPath] = val;
	                    store.setValue(item.parent, item.property, storeItemVal);
	                }
	            }
        	}catch(e){
        		alert(e);
        	}
        }

        if(editDialog.validate()){
            node = this.lastFocused;
            item = node.item;
            var value = item.value;
            // var property = null;
            if (item.addNew) {
                // we are adding a property to the parent item
                // the real value of the parent is in the parent property of the lastFocused item
                // this.lastFocused.getParent().item.value may be a reference to an item
                value = node.item.parent;
                node = node.getParent();
                item = node.item;
            }
            val = null;
            switch(vals.itemType){
                case "reference":
                    this.store.fetchItemByIdentity({identity:vals._reference,
                    	onItem:function(item){
                    		val = item;
                    		setValue();
                    	},
                    	onError:function(){
                    		alert("The id could not be found");
                    	}
                	});
                    break;
                case "value":
                	var jsonVal = vals.jsonVal;
                    val = dojo.fromJson(jsonVal);
                    // if it is a function we want to preserve the source (comments, et al)
                    if(typeof val == 'function'){
                    	val.toString = function(){
                    		return jsonVal;
                    	}
                    }
                    setValue();
                    break;
            }
        }else{
            // the form didn't validate - show it again.
            editDialog.show();
        }
    },
    _editProperty: function(){
        // this mixin stops us polluting the tree item with jsonVal etc.
        // FIXME: if a store identifies items by instanceof checks, this will fail
        var item = dojo.mixin({}, this.lastFocused.item);
        // create the dialog or reset it if it already exists
        if(!this._editDialog){
            this._createEditDialog();
        }else{
            this._editDialog.reset();
        }
        // not allowed to edit an item's id - so check for that and stop it.
        if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
            alert("Cannot Edit an Identifier!");
        }else{
            this._editDialog.attr("title", "Edit Property");
            // make sure the property input is disabled
            dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true);
            if(this.store.isItem(item.value, true)){
                // root node || Item reference
                if(item.parent){
                    // Item reference
                    item.itemType = "reference";
                    this._enableFields(item.itemType);
                    item._reference = this.store.getIdentity(item.value);
                    this._editDialog.attr("value", item);
                    this._editDialog.show();
                } // else root node
            }else{
                if(item.value && typeof item.value == 'object' && !(item.value instanceof Date)){
                    // item.value is an object but it's NOT an item from the store - no-op
                    // only allow editing on a property not on the node that represents the object/array
                }else{
                    // this is a primitive
                    item.itemType = "value";
                    this._enableFields(item.itemType);
                    item.jsonVal = typeof item.value == 'function' ?
                    		// use the plain toString for functions, dojo.toJson doesn't support functions 
                    		item.value.toString() :
                    			item.value instanceof Date ?
                    				// A json-ish form of a date:
                    				'new Date("' + item.value + '")' : 
                    				dojo.toJson(item.value);
                    this._editDialog.attr("value", item);
                    this._editDialog.show();
                }
            }
        }
    },
    _destroyProperty: function(){
        var node = this.lastFocused;
        var item = node.item;
        var propPath = [];
        // we have to walk up the tree to the item before we can know if we're working with the identifier
        while(!this.store.isItem(item.parent, true) || item.parent instanceof Array){
            node = node.getParent();
            propPath.push(item.property);
            item = node.item;
        }
        // this will prevent any part of the identifier from being changed
        if(dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
            alert("Cannot Delete an Identifier!");
        }else{
        	try{
	            if(propPath.length > 0){
	                // not deleting a top-level property of an item so get the top-level store item to change
	                var itemVal, storeItemVal = getValue(this.store, item.parent, item.property);
	                itemVal = storeItemVal;
	                // walk back down the object if needed
	                while(propPath.length > 1){
	                    itemVal = itemVal[propPath.pop()];
	                }
	                // delete the property
	                if(dojo.isArray(itemVal)){
	                    // the value being deleted represents an array element
	                    itemVal.splice(propPath, 1);
	                }else{
	                    // object property
	                    delete itemVal[propPath];
	                }
	                // save it back to the store
	                this.store.setValue(item.parent, item.property, storeItemVal);
	            }else{
	                // deleting an item property
	                this.store.unsetAttribute(item.parent, item.property);
	            }
        	}catch(e){
        		alert(e);
        	}       
        }
    },
    _addProperty: function(){
        // item is what we are adding a property to
        var item = this.lastFocused.item;
        // value is the real value of the item - not a reference to a store item
        var value = item.value;
        var showDialog = dojo.hitch(this, function(){
            var property = null;
            if(!this._editDialog){
                this._createEditDialog();
            }else{
                this._editDialog.reset();
            }
            // are we adding another item to an array?
            if(value instanceof Array){
                // preset the property to the next index in the array and disable the property field
                property = value.length;
                dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", true);  
            }else{
                // enable the property TextBox
                dijit.getEnclosingWidget(dojo.query("input", this._editDialog.containerNode)[0]).attr("disabled", false);
            }
            this._editDialog.attr("title", "Add Property");
            // default to a value type
            this._enableFields("value");
            this._editDialog.attr("value", {itemType: "value", property: property});
            this._editDialog.show();
        });
        
        if (item.addNew) {
            // we are adding a property to the parent item
            item = this.lastFocused.getParent().item;
            // the real value of the parent is in the parent property of the lastFocused item
            // this.lastFocused.getParent().item.value may be a reference to an item
            value = this.lastFocused.item.parent;
        }
        if(item.property && dojo.indexOf(this.store.getIdentityAttributes(), item.property) >= 0){
            alert("Cannot add properties to an ID node!");
        }else{
            // if the value is an item then we need to get the item's value
            if(this.store.isItem(value, true) && !this.store.isItemLoaded(value)) {
                // fetch the value and see if it is an array
                this.store.loadItem({
                    item: value,
                    onItem: function(loadedItem){
                        value = loadedItem;
                        showDialog();
                    }
                });
            } else {
                showDialog();
            }
//
        }
    }
});
})();


}
